package org.sf.comp.tagging;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 
 * The tag manager class determines what tags something has.
 *
 */
public class TagManager implements Tag {

	private Collection<Tag> tags = new HashSet<Tag>();
	private List<Object> list = new ArrayList<Object>(tags);

	// instance variables
	private String name = null;

	/**
	 * Constructs .
	 */
	public TagManager(String aName) {
		name = aName;
	}

	public TagManager()
	{
		// TODO Auto-generated constructor stub
	}

	/**
	 * Finds a tag by its common name
	 * @param name	the name of the tag to find
	 * @return	the tag or null
	 */
	public Tag find(String name) {
		Tag tagToFind = null;
		for(Tag tag : tags) {
			if(tag.getName().equals(name)){
				tagToFind = tag;
				break;
			}
		}
		return tagToFind;
	}

	/**
	 * Method to ensure that this class is tracking the tags you're linking.
	 * Only necessary if you manually connect tags instead of using the
	 * linkTagToMany or tagObject methods.
	 * 
	 * @param tag	Tags to manage
	 */
	public void register(Tag... tag) {
		tags.addAll(Arrays.asList(tag));
	}

	/**
	 * Tags one thing with many tags
	 * @param something	The thing to be tagged.
	 * @param tags	The tags to tag it with.
	 */
	public void tagObject(Object something, Tag... tags) {
		for(Tag tag : tags) {
			register(tag);
			tag.associate(something);
		}
	}

	/**
	 * Tags many things with one tag.
	 * @param tag	The tag to use.
	 * @param toBeTagged	The things to be tagged.
	 */
	public void linkTagToMany(Tag tag, Object... toBeTagged){
		register(tag);
		for(Object o : toBeTagged) {
			if(o instanceof Tag) {
				register((Tag) o);
			}
		}
		tag.associate(toBeTagged);
	}

	/**
	 * 
	 * @return	all Tags under management
	 */
	public List<Tag> listTags() {
		return new ArrayList<Tag>(tags);
	}

	/**
	 * 
	 * @param target	the object to start searching from
	 * @param searchDepth	the number of tags to "hop" from the target
	 * @return	all objects, including tags, within range of the target object
	 */
	public Set<Object> related(Object target, int searchDepth) {
		Set<Object> relatedElements = new HashSet<Object>();
		Set<Object> elementsToSearchFrom = new HashSet<Object>();
		elementsToSearchFrom.add(target);
		for(int i = 0; i < searchDepth; i++){
			Set<Object> foundElements = new HashSet<Object>();
			for(Object object : elementsToSearchFrom) {
				foundElements.addAll(search(object));
				relatedElements.addAll(foundElements);
			}
			elementsToSearchFrom.clear();
			elementsToSearchFrom.addAll(foundElements);

		}
		relatedElements.remove(target);
		return relatedElements;
	}

	/**
	 * 
	 * @param target	the object to start searching from
	 * @param searchDepth	the number of tags to "hop" from the target	
	 * @param clazz	the type of objects to return
	 * @return	all objects of type clazz, including tags, within range of the target object
	 */
	public <T> Set<T> related(Object target, int searchDepth, Class<T> clazz) {
		Set<T> related = new HashSet<T>();
		Set<Object> allRelated = related(target, searchDepth);
		for(Object object : allRelated) {
			if(clazz.isInstance(object)) {
				related.add(clazz.cast(object));
			}
		}
		return related;
	}

	private Set<Object> search(Object o) {
		Set<Object> relatedElements = new HashSet<Object>();
		if(Tag.class.isInstance(o)){
			Tag theTag = Tag.class.cast(o);
			relatedElements.addAll(theTag.listAssociated());
		} else {
			for(Tag tag : tags) {
				if(tag.listAssociated().contains(o)){
					relatedElements.add(tag);
				}
			}
		}
		return relatedElements;
	}

	/**
	 * 
	 * @param target	the object to start searching from
	 * @param searchDepth	the number of tags to "hop" from the target	
	 * @param classToExclude	the type of objects to not return
	 * @return	all objects except of type classToExclude, including tags, within range of the target object
	 */
	public <T> Set<Object> relatedExcept(Object target, int searchDepth, Class<T> classToExclude) {
		Set<Object> related = new HashSet<Object>();
		Set<Object> allRelated = related(target, searchDepth);
		for(Object object : allRelated) {
			if(!classToExclude.isInstance(object)) {
				related.add(object);
			}
		}
		return related;
	}

	@Override
	public String getName()
	{
		// TODO Auto-generated method stub
		return name;
	}

	@Override
	public void setName(String name)
	{
		// TODO Auto-generated method stub
		this.name = name;
		
	}

	@Override
	public void associate(Object... o)
	{
		list.add(o);
		// TODO Auto-generated method stub		
	}

	@Override
	public void disassociate(Object... o)
	{
		list.remove(o);
		// TODO Auto-generated method stub
	}

	@Override
	public List<Object> listAssociated()
	{
		//List<Object> list = new ArrayList<Object>(tags);
		return list;
	}

}
